package cl.sujey.arrays;

import cl.sujey.utils.ArrayUtils;

public class Matrix {

    private double[][] matrix;

    /**
     * TODO: Comentar este metodo.
     *
     * @param row
     * @param column
     */
    public Matrix(int row, int column) throws Exception {

        if (row <= 0 || column <= 0)
            throw new Exception(
                    "Las dimensiones de la matriz no pueden ser negativas");

        this.matrix = new double[row][column];
    }

    /**
     * Fija un valor en una posicion de la matriz.
     *
     * @param position La posicion en la cual va el valor.
     * @param value    El valor a fijar
     * @throws PositionOutOfBoundsException Arrojada si la posicion se encuentra fuera de los limites de
     *                                      la matriz
     */
    public void setValue(Position position, double value)
            throws PositionOutOfBoundsException {

		/* Se valida que la posicion este dentro del rango */
        if (isOutOfBounds(position)) {
            throw new PositionOutOfBoundsException();
        }

        int row = position.getRow();
        int column = position.getColumn();

        matrix[row - 1][column - 1] = value;

    }

    public double getValue(Position position)
            throws PositionOutOfBoundsException {

        if (this.isOutOfBounds(position))
            throw new PositionOutOfBoundsException();

        return matrix[position.getRow() - 1][position.getColumn() - 1];
    }

    /**
     * TODO: TAREA!
     *
     * @param position indica ubicacion dentro o fuera de la matriz
     * @return si se encuentra o no dentro de la matriz
     */
    protected boolean isOutOfBounds(Position position) {

		/* La posicion indica una fila y columna: las recuperamos */
        int row = position.getRow();
        int column = position.getColumn();

        if ((row < 1) || (column < 1)) {
            return true;
        }

        int rows = matrix.length;
        int columns = matrix[0].length;

        if ((row > rows) || (column > columns)) {
            return true;
        }

        return false;
    }

    /**
     * Este método es responsable de sumar a ESTA (this) matriz, la matriz dada
     * como argumento.
     *
     * @param aMatrix La matriz a la cual se suma esta matriz.
     * @throws DifferentSizeException Se arroja si las matrices no son del mismo tama�o.
     */
    public void sum(Matrix aMatrix) throws DifferentSizeException {

		/* Se valida que las matrices sean del mismo tama�o */
        if ((aMatrix.getColumns() != this.getColumns())
                || (aMatrix.getRows() != this.getRows())) {
            throw new DifferentSizeException();
        }

        for (int row = 0; row < matrix.length; row++) {
            for (int column = 0; column < matrix[row].length; column++) {
                /* El valor a sumar */
                Position currentPosition = new Position(row, column);
                double aValue;
                try {
                    aValue = aMatrix.getValue(currentPosition);
                } catch (PositionOutOfBoundsException e) {
                    throw new DifferentSizeException();
                }
                matrix[row][column] = matrix[row][column] + aValue;
            }
        }
    }

    /**
     * TODO: TAREA PARA LA CASA! Este metodo es responsable de multiplicar ESTA
     * (this) matriz por la matriz que se recibe como parametro (a).
     *
     * @param aMatrix La matriz por la cual se multiplicara.
     * @throws Exception Error si las matrices no se pueden multiplicar
     */
    public void multiplication(Matrix aMatrix) throws Exception {

		/* Ver si la multiplicacion es posible */
        if (!aMatrix.isValid())
            return;

		/*
         * TODO: Agregar la otra condicion para ver si se pueden multiplicar las
		 * matrices
		 */
        if (this.getColumns() != aMatrix.getRows()) {
            throw new Exception("Las matrices no son multiplicables: numero de filas de esta matriz no coincide con el numero de columnas de la segunda matriz.");
        }

		/* Determinar el "nuevo" tama�o de esta matriz (si fuera distinto) */
        int newRowNumber = this.matrix.length;
        int newColumnNumber = aMatrix.matrix[0].length;
        double result[][] = new double[newRowNumber][newColumnNumber];

		/* Calcular el valor de cada posicion de la matriz resultante */
        for (int iRow = 1; iRow <= newRowNumber; iRow++) {
            for (int iCol = 1; iCol <= newColumnNumber; iCol++) {
                double[] theRow = this.getRow(iRow);
                double[] theCol = aMatrix.getColumn(iCol);

                result[iRow - 1][iCol - 1] = ArrayUtils
                        .multiply(theRow, theCol);
            }
        }

        this.matrix = result;
    }

    /**
     * This method is responsible of retrieving a given column from this matrix.
     *
     * @param iCol The column number to retrieved from the matrix.
     * @return The column retrieved.
     * @throws ColumnOutOfRangeException If the given column number (<code>iCol</code>) is out of the matrix range
     *                                   (less than 1, or greater than the number of columns).
     */
    public double[] getColumn(int iCol) throws ColumnOutOfRangeException {

        /* Validations go first */
        if (iCol <= 0) {
            throw new ColumnOutOfRangeException("The column number can not be less than zero.");
        }

        if (iCol > this.getColumns()) {
            throw new ColumnOutOfRangeException("The column number can not be greater than the matrix number of columns (" + this.getColumns() + ").");
        }

        double[] theColumn = new double[this.getColumns()];

        for (int i = 0; i < theColumn.length; i++) {
            theColumn[i] = this.matrix[i][iCol - 1];
        }
        return theColumn;
    }

    public double[] getRow(int iRow) throws Exception {

        if ((iRow < 0) || (iRow > this.getRows())) {
            throw new Exception("Numero de fila (" + iRow + ") fuera de limites de la matriz.");
        }

        return matrix[iRow - 1];
    }

    /**
     * Este método es responsable de determinar si esta es una matriz v�lida o
     * no. Es decir, si sus dimensiones son positivas.
     *
     * @return <code>true</code> si es v�lida y <code>false</code> sino.
     */
    protected boolean isValid() {
        if (this.matrix.length > 0) {
            return true;
        }

        return false;
    }

    public int getRows() {
        return this.matrix.length;
    }

    public int getColumns() {
        return this.matrix[0].length;
    }

    /**
     * TODO: Este método es responsable de verificar si esta matriz es el cero.
     *
     * @return <code>true</code> si es la matriz cero y <code>false</code> sino.
     */
    public boolean isZero(Position position) {

        int row = position.getRow();
        int column = position.getColumn();

        double value = this.matrix[row - 1][column - 1];

        return (value == 0);
    }

    @Override
    public String toString() {

        String representation = "[ ";

        for (int row = 0; row < matrix.length; row++) {

            representation += " row " + (row + 1) + ": { ";

			/* Este bucle imprime los elementos de la fila */
            for (int column = 0; column < matrix[row].length; column++) {
                representation += matrix[row][column] + " ";
            }

            representation += "}";
        }

        return representation;
    }
}
